﻿////////////////////////////////////////////////////////////////////////////////////////////////
//
// Copyright © Yaroslavov Alexander 2010
//
// Contacts:
// Phone: +7(906)827-27-51, +7(964)595-55-94
// Email: x-ronos@yandex.ru
//
/////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Moppet.Lapa.Parsers
{

    /// <summary>
    /// Парсер даты и времени.
    /// Список форматов: http://msdn.microsoft.com/ru-ru/library/saw4x629.aspx.
    /// </summary>
    public static class LpDateTime
    {


        /// <summary>
        /// Конструирует парсер времени в следующих форматах (hh:mm:ss | hh:mm | hh:mm:ss.fff).
        /// Часы, минуты, секунды и миллисекунды помечаются как "h", "m", "s" и "f" соответственно.
        /// Все разделители помечаются пробелом.
        /// </summary>
        /// <returns>Парсер.</returns>
        public static LpsParser Time()
        {
            var hour  = LpNumber.Positive(0, 23,  1).Rename("h");
            var min   = LpNumber.Positive(0, 59,  1).Rename("m");
            var sec   = LpNumber.Positive(0, 59,  1).Rename("s");
            var msec  = LpNumber.Positive(0, 999, 1).Rename("f");
            var delim = Lp.Char(':').Id(" ");

            return (hour + delim + min).ToParser().MaybeNext((delim + sec).NextZeroOrMore(Lp.Char('.').Id(" ") + msec)).Id("Time"); 
        }

        /// <summary>
        /// Конструирует парсер даты в следующих вариантах:
        /// (28/07/2009 | 28.07.2009 | 28-07-2009 | 2009/7/28 | 2009.7.28 | 2009-7-28 | 0001-7-28 | 28/07/09 | 28.07.99 | 28-07-09).
        /// Последние три формата однозначно не определяются, т.к. есть небольшая вероятность, что мы разберём дату, где год вначале, а день в конце.
        /// Разделитель помечается символом '-', год, месяц и день, соответственно, знаками 'Y', 'M', 'D'.
        /// </summary>
        /// <returns>Парсер.</returns>
        public static LpsParser Date()
        {
            var year  = LpNumber.Positive(0, 9999, 4).Rename("Y");
            var month = LpNumber.Positive(1, 12,   1).Rename("M");
            var day   = LpNumber.Positive(1, 31  , 1).Rename("D");

            var dMinus     = Lp.Char('-' ).Rename("-");
            var dPoint     = Lp.Char('.' ).Rename("-");
            var dSlash     = Lp.Char('/' ).Rename("-");
            var dBackSlash = Lp.Char('\\').Rename("-");

            // Какие бы ни были варианты, месяц всегда по середине.

            // 28/07/2009 | 28.07.2009 | 28-07-2009 | 
            var caseMinus1  = (dMinus + month + dMinus + year).ToParser().Parser;
            var casePoint1  = (dPoint + month + dPoint + year).ToParser().Parser;
            var caseSlash1  = (dSlash + month + dSlash + year).ToParser().Parser;
            var caseBSlash1 = (dBackSlash + month + dBackSlash + year).ToParser().Parser;
            
            var date1 = day.Switch((c, t) => { switch (c)
            {
                case '-' : return caseMinus1(t);
                case '.' : return casePoint1(t);
                case '/' : return caseSlash1(t);
                case '\\': return caseBSlash1(t);
                default  : return new LpNode(t);
            }});

                
            // 2009/7/28 | 2009.7.28 | 2009-7-28 | 0001-7-28
            var caseMinus2  = (dMinus     + month + dMinus     + day).ToParser().Parser;
            var casePoint2  = (dPoint     + month + dPoint     + day).ToParser().Parser;
            var caseSlash2  = (dSlash     + month + dSlash     + day).ToParser().Parser;
            var caseBSlash2 = (dBackSlash + month + dBackSlash + day).ToParser().Parser;
            var date2 = year.Switch((c, t) => { switch (c)
            {
                case '-' : return caseMinus2(t);
                case '.' : return casePoint2(t);
                case '/' : return caseSlash2(t);
                case '\\': return caseBSlash2(t);
                default  : return new LpNode(t);
            }});

                
            // Следующие варианты однозначно определяются, если день больше 12-ти
            day   = LpNumber.Positive(1,  31, 2).Rename("D");
            month = LpNumber.Positive(1,  12, 2).Rename("M");
            year  = LpNumber.Positive(0,  99, 2).Rename("Y");

            // 28/07/09 | 28.07.09 | 28-07-09
            var caseMinus3  = (dMinus     + month + dMinus     + year).ToParser().Parser;
            var casePoint3  = (dPoint     + month + dPoint     + year).ToParser().Parser;
            var caseSlash3  = (dSlash     + month + dSlash     + year).ToParser().Parser;
            var caseBSlash3 = (dBackSlash + month + dBackSlash + year).ToParser().Parser;
            var date3 = day.Switch((c, t) => { switch (c)
            {
                case '-' : return caseMinus3(t);
                case '.' : return casePoint3(t);
                case '/' : return caseSlash3(t);
                case '\\': return caseBSlash3(t);
                default  : return new LpNode(t);
            }});

            //var date = Lp.If
            //(
            //    condition: (c) => c >= '0' && c <= '9',
            //    ifTrue   : (date1 | date2 | date3).TakeFirst(),
            //    ifFalse  : Lp.Fail
            //);
            var date = (date1 | date2 | date3).TakeFirst();
            var result = Lp.Switch((c, t) => (c >= '0' && c <= '9') ? date.Parser(t) : new LpNode(t));
            return result.Id("Date");
        }


        /// <summary>
        /// Builds parser of date and time. The combination of two parsers 'Date()' and 'Time()'.
        /// Конструирует парсер даты и времени, комбинация парсеров Date() и Time().
        /// </summary>
        /// <returns>Parser.</returns>
        public static LpsParser DateTime()
        {
            var date   = Date().Parser;
            var time   = Time().Parser;
            var spaces = Lp.OneOrMore(' ').Parser;
            const string dtId = "DateTime";
            const string dId  = "Date";
            const string tId  = "Time";

            return new LpsParser(null, (t) =>
            {
                var nDate = date(t); nDate.Id = dId;
                if (nDate.Match.Length < 0)
                    return new LpNode(dtId, t);
                var nSpace = spaces(nDate.Rest); nSpace.Id = " ";
                if (nSpace.Match.Length < 0)
                    return new LpNode(dtId, nDate.Match, nDate.Rest, nDate);
                var nTime = time(nSpace.Rest); nTime.Id = tId;
                if (nTime.Match.Length < 0)
                    return new LpNode(dtId, nDate.Match, nDate.Rest, nDate);
                return new LpNode(t, nTime.Rest.Index - t.Index, dtId,  nDate, nSpace, nTime);
            });
        }

        /// <summary>
        /// Разбирает результат работы парсера Time().
        /// В случае успеха записывает в time время.
        /// </summary>
        /// <param name="n">Результат парсера.</param>
        /// <param name="time">Переменная, куда записывается время.</param>
        /// <returns>Истина, если удалось разобрать все компоненты времени и записать результат в time.</returns>
        public static bool TryParseTime(LpNode n, ref TimeSpan time)
        {
            if (n.Children == null || !n.Success)
                return false;

            int h = 0, m = 0, s = 0, f = 0, ok = 0;
            foreach (var cn in n.Children)
            {
                switch (cn.Id)
                {
                    case "h":
                        if (!int.TryParse(cn.Match.ToString(), out h))
                            return false;
                        ok |= 1;
                        break;
                    case "m":
                        if (!int.TryParse(cn.Match.ToString(), out m))
                            return false;
                        ok |= 2;
                        break;
                    case "s":
                        if (!int.TryParse(cn.Match.ToString(), out s))
                            return false;
                        break;
                    case "f":
                        if (!int.TryParse(cn.Match.ToString(), out f))
                            return false;
                        break;
                }
            }
            if (ok != 3)
                return false;

            try
            {
                time = new TimeSpan(days: 0, hours: h, minutes: m, seconds: s, milliseconds: f);
            }
            catch
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Разбирает результат работы парсера Date().
        /// В случае успеха записывает в date только дату, но не время.
        /// </summary>
        /// <param name="n">Результат парсера.</param>
        /// <param name="date">Переменная, куда записывается дата.</param>
        /// <returns>Истина, если удалось разобрать все компоненты даты и записать результат в date.</returns>
        public static bool TryParseDate(LpNode n, ref DateTime date)
        {
            if (n.Children == null || !n.Success)
                return false;

            int year = -1, month = -1, day = -1, ok = 0;
            foreach (var cn in n.Children)
            {
                switch (cn.Id)
                {
                    case "Y":
                        if (!int.TryParse(cn.Match.ToString(), out year))
                            return false;
                        ok |= 1;
                        break;
                    case "M":
                        if (!int.TryParse(cn.Match.ToString(), out month))
                            return false;
                        ok |= 2;
                        break;
                    case "D":
                        if (!int.TryParse(cn.Match.ToString(), out day))
                            return false;
                        ok |= 4;
                        break;
                }
            }
            if (ok != (1 | 2 | 4))
                return false;
            
            try
            {
                date = new DateTime(year, month, day);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }
}
